Clean Code - Chapter 4 注释

2016-11-12 21:44

作者:给立乐*
出处:http://spencer-dev.com/2016/11/12/Clean Code - Chapter 4 注释
声明:本文采用以下协议进行授权: 自由转载-非商用-非衍生-保持署名|Creative Commons BY-NC-ND 3.0 ,转载请注明作者及出处。

“别给糟糕的代码加注释 - 重新写吧” —— Brian W. Kernaghan 与 P.J.Plaugher。

什么也比不上放置良好的代码来的有用。什么也不会比乱七八糟的注释更有本事搞乱一个模块。什么也不会比陈旧、提供错误信息的注释更有破坏性。

注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。

我为什么要极力贬低注释?

因为注释会撒谎。注释存在的时间越久,就离其所描述的代码越远,越来越变的全然错误。原因很简单。程序员不能坚持维护注释。

代码在变动,在演化。从这里移到那里。彼此分离,重造又合到一处。很不幸,注释并不总是随之变动——不能总是跟着走。注释常常会与其所描述的代码分隔开来,孑然飘零,越来越不准确。

程序员应当负责将注释保持在可维护、精确的高度。我同意这种说法。但我更主张把力气用在写清楚代码上,直接保证无需编写注释。

不准确的注释要比没注释坏的多。它们满口胡言。它们预期的东西永远不能实现。它们设定了无需也不应在遵循的旧规则。

真想只在一处地方有:代码。只有代码能忠实地告诉你它做的事。那是唯一真正准确的信息来源。所以,尽管有时也需要注释,我们也该多花心思尽量减少注释量。

注释不能美化糟糕的代码

写注释的常见动机之一是糟糕的代码的存在。我们告诉自己:“喔,最好写点注释!” 不!最好是把代码弄干净!

带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多。与其花时间编写解释你搞出的糟糕代码的注释,不如花时间清洁那堆糟糕的代码。

用代码来阐述

有时,代码本身不足以解释其行为。

比如:

1
2
// Check to see if the employee is eligible for full benefits
if((employee.flags & HOURLY_FLAG) && (employee.age > 65))

其实只需要想上哪么几秒钟,就能用代码解释大部分意图。

1
if(employee.isEligibleForFullBenefits())

很多时候,简单到需要创建一个描述与注释所言同一事物的函数即可。


好注释

有些注释是必须的,也是有利的。但真正好的注释是你想办法不去写注释。下面看一些值得写的注释。

法律信息

有时,公司代码规范要求编写与法律有关的注释。例如,版权及著作权声明就是必须和有理由在每个源文件开头注释处放置的内容。

这类注释不应是合同或法典。只要有可能,就只想一份标准许可或其它外部文档,而不要把所有条款放到注释中。

提供信息的注释

有时,用注释来提供基本信息也有其用处。

例如,以下注释解释了某个抽象方法的返回值:

1
2
// Returns an instance of the Responder being tested
protected abstract Responder responderInstance();

这类注释有时管用,但更好的方式是尽量利用函数名称传达信息。

比如,在本例中,只要把函数重命名为 responderBeingTested,注释就是多余的了。

对意图的解释

有时,注释不仅提供了有关实现的有用信息,而且还提供了某些后续的意图。

阐释

有时,注释把某些晦涩难明的参数或返回值的意义翻译为某种可读形式,也会是有用的。通常,更好的方法是尽量让参数或返回值自身就足够清楚;但如果参数或返回值是某个标准库的一部分,或是你不能修改的代码,帮助阐释其含义就会很有用。

例如:

1
2
3
4
5
6
7
public void testCompareTo() throws Exception {
WikiPagePath a = PathParser.parse("PageA");
WikiPagePath ab = PathParser.parse("PageA.PageB");
asserTrue(a.compareTo(a) == 0); // a == a
asserTrue(ab.compareTo(ab) == 0); // ab == ab
}

当然,这也会冒着阐释性注释本身就不正确的风险。回头看看上例,你会发现想要确认注释的正确性有多难。这一方面说明了阐释有多必要,另外也说明了它有风险。所以,在写这类注释之前,考虑一下是否还有更好的办法,然后再加倍小心地确认注释正确性。

警示

有时,用于警告其他程序员会出现某种后果的注释也是有用的。

1
2
3
4
5
6
7
8
9
10
// Don`t run unless you
// have some time to kill.
public static void _testWithReallyBigFile() {
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToSend(this);
String responseString = output.toString();
assertSubString("Content-Length:1000000000");
assertTrue(bytesSent > 1000000000);
}

TODO 注释

有时,有理由用 // TODO 形式在源代码中放置要做的工作列表。

例如:

1
2
3
4
5
// TODO-MdM these are not needed
// We expect this to go away when we do the checkout model
protected VersionInfo makeVersion() throws Exception {
return null;
}

TODO 是一种程序员认为应该做,但由于某些原因目前还没做的工作。它可能是要提醒删除某个不必要的特性,或者要求他人注意某个问题。它可能是恳请别人取个好名字,或者对提示依赖于某个计划事件的修改。无论 TODO 的目的如何,它都不是在系统中留下糟糕代码的借口。

如今,大多数的 IDE 都提供了特别的手段来定位所有 TODO 注释,这些注释看来丢不了。你不会愿意代码因为 TODO 的存在而变成一堆垃圾,所以要定期查看,删除不再需要的。

放大

注释可以用来放大某种看来不合理之物的重要性。

例如:

1
2
3
4
5
6
String listItemContent = match.group(3).trim();
// he trim is real important. It removes the starting
// spaces that could cause the item to be recognized
// as another list.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.subString(match.end()));

公共 API 中的 Javadoc

如果你在编写公共 API,就该为他编写良好的 Javadoc。不过要记住本章中的其他建议。就像其他注释一样,Javadoc 也可能误导、不适用或者提供错误信息。


坏注释

大多数注释都属此类。通常,坏注释都是糟糕代码的支撑或借口,或者对错误决策的修正,基本上等于程序员的自说自话。

喃喃自语

如果只是因为你觉得应该或因为过程需要就添加注释,那就是无谓之举。如果你决定写注释,就要花必要的事件来确保写出最好的注释。

任何迫使读者查看其他模块的注释,都没能与读者沟通好,不值所费。

多余的注释

下面这段代码是一个简单的函数,其头部位置的注释全属多余。读这段注释花的时间没准比读代码花的时间还长。

1
2
3
4
5
6
7
8
9
10
// Utility method that returns when this. closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis) throws Exception {
if(!closed) {
wait(timeoutMillis);
}
if(!closed) {
throw new Exception("MockResponseSender could not be closed");
}
}

这段注释起了什么作用?它并不能比代码本身提供更多的信息。他没有证明代码的意义,也没有给出代码的意图或逻辑。读它并不比读代码更容易。事实上,它不如代码精确,还误导读者接受不精确的信息,而不是正确的理解代码。

下面来看看摘自 Tomcat 项目的无用而多余的 Javadoc。这些注释只是一味将代码搞的含糊不明。完全没有文档上的价值。

ContainerBase.java

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class ContainerBase implements Container, Lifecycle, Pipeline, MBeanRegistration, Serializable {
/**
* The processor delay for this component.
*/
protected int backgroundProcessorDelay = -1;
/**
* The lifecycle event support for this component.
*/
protected LifecycleSupport lifecycle = new LifecyclesSupport(this);
// ....
}

误导性注释

有时,尽管初中可嘉,程序员还是会写出不够精确的注释。一些细微的误导信息,放在比代码本身更难阅读的注释里面,有可能导致其他程序员快活地调用函数,这位可怜的程序员将会发现自己陷于调试的困境之中。

循规式注释

所谓每个函数都要有 Javadoc 或每个变量都要有注释的规矩全然是愚蠢可笑的。这类注释徒然让代码变得散乱,满口胡言,令人迷惑不解。这类废话只会搞乱代码,有可能误导读者。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
*
* @param title The title of the CD
* @param author The author of the CD
* @param tracks The number of tracks on the CD
* @param durationInMinutes The duration of the CD in minutes
*/
public void addCD(String title, String author, int tracks, int durationInMinutes) {
CD cd = new CD();
cd.title = title;
cd.author = author;
cd.tracks = tracks;
cd.duration = duration;
cdList.add(cd);
}

日志式注释

有人会在每次编辑代码时,在模块开始处添加一条注释。这类注释就像是一种记录每次修改的日志。

很久以前,在模块开始处创建并维护这些记录还算有道理。那时,我们还没有源代码控制系统可用。如今,这种冗长的记录只会让模块变得凌乱不堪,应当全部删除。

废话注释

有时,你会看到纯然是废话的注释。它们对于显然之事喋喋不休,毫无新意。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Default constructor.
*/
protected AnnualDateRule() {
}
/** The day of the month. */
private int dayOfMonth;
/**
* Returns the day of the month.
*
* @return the day of the month.
*/
public int getDayOfMonth() {
return dayOfMonth;
}

这类注释废话连篇,我们都学会了视而不见。读代码时,眼光不会停留在它们上面。最终,当代码修改之后,这类注释就变作了谎言一堆。

下面代码中,第一条注释貌似还行。它解释了 catch 代码块为何被忽略。不过第二条注释就是纯废话了。显然,该程序员沮丧于编写函数中的那些 try/catch 代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void startSending()
{
try
{
doSending();
}
catch(SocketException e)
{
// normal. someone stopped the request.
}
catch(Exception e)
{
try
{
response.add(ErrorResponser.makeExceptionString(e));
response.closeAll();
}
catch(Exception e1)
{
// Give me a break!
}
}
}

与其纠缠于毫无价值的废话注释,程序员应该意识到,他的挫败感可以由改进代码结构而消除。他应该把力气花在将最末一个 try/catch 代码块拆解到单独的函数中,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void startSending()
{
try
{
doSending();
}
catch(SocketException e)
{
// normal. someone stopped the request.
}
catch(Exception e)
{
addExceptionAndCloseResponse(e);
}
}
private void addExceptionAndCloseResponse(Exception e)
{
try
{
response.add(ErrorResponder.makeExceptionString(e));
response.closeAll();
}
catch(Exception e1)
{
}
}

用整理代码的决心替代创造废话的冲动。你会发现自己成为更优秀、更快乐的程序员。

可怕的废话

Javadoc 也有可能是废话。下列 Javadoc(来自某知名开源库)的目的是什么?答案:无。

1
2
3
4
5
6
7
8
/** The name.*/
private String name;
/** The version.*/
private String version;
/** The licenceName.*/
private String licenceName;
/** The version.*/
private String info;

再仔细读读这些注释。你是否发现了剪切-粘贴错误?如果作者在写(或粘贴)注释时都没花心思,怎么能指望读者从中获益呢?

能用函数或变量时就别用注释

看看以下代码概要:

1
2
3
4
5
// does the module from the global list <mod> depend on the
// subsystem we are part of?
if(smodule.getDependSubsystems().contains(SubSysMod.getSubSystem())) {
}

可以改成一下没有注释的版本:

1
2
3
4
5
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if(moduleDependees.contains(ourSubSystem)) {
}

位置标记

有时,程序员喜欢早源代码中标记某个特别的位置。例如我最近在程序中看到这样一行:

1
// Actions /////////////////////////////////////////////

把特定函数趸放在这种标记栏下面,多数时候实属无理。鸡零狗碎,理当删除 - 特别是尾部那一长串无用的斜杠。

这么说吧。如果标记栏不多,就会显而易见。所以,尽量少用标记栏,只在特别有价值的时候用。如果滥用标记栏,就会沉默在背景噪音中,被忽略掉。

括号后面的注释

有时,程序员会在括号后面放置特殊的注释,如下面代码所示。

尽管这对于含有深度嵌套结构的长函数可能有意义,但只会给我们愿意编写的短小、封装的函数带来混乱。如果你发现自己想标记右括号,其实应该做的是缩短函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class wc {
public static void main(String[] args) {
try {
// ....
while((line = in.readLine()) != null) {
// ....
} // while
} // try
catch(IOException e) {
// ....
} // catch
} // main
}

归属与署名

1
/* Added by Rick */

源代码控制系统非常善于记住是谁在何时添加了什么。没必要用那些小小的签名搞脏代码。你也许会认为,这种注释大概有助于他人了解和谁讨论这段代码。不过,事实却是注释在那儿放了一年又一年,越来越不准确,越来越和原作者没关系。

重申一下,源代码控制系统是这类信息最好的归属地。

注释掉的代码

直接把代码注释掉是讨厌的做法。别这么干!

1
2
3
4
5
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));

其他人不敢删除注释掉的代码。他们会想,代码依然放在那儿,一定有其原因,而且这段代码很重要,不能删除。注释掉的代码堆积在一起,就像破酒瓶底掉渣在一般。

代码为什么要注释掉?它们重要吗?它们搁在那儿,是为了给未来的修改做提示吗?或者,只是某人在多年以前注释掉、懒得清理的过时玩意儿?

20世纪60年代,曾经有那么一段时间,注释掉的代码可能有用。但我们已经拥有了优良的源代码控制系统如此之久,这些系统可以为我们记住不要的代码。我们无需再用注释来标记,删掉即可,它们丢不了。

HTML 注释

源代码注释中的 HTML 标签是一种厌恶。编辑器/IDE 中的代码本来易于阅读,却因为 HTML 注释的存在变得难以卒读。如果注释将由某种工具(例如 Javadoc)抽取出来,呈现到网页,那么该是工具而非程序员来负责给注释加上合适的 HTML 标签。

非本地信息

假如你一定要写注释,请确保它描述了离它最近的代码。别再本地注释的上下文环境中给出系统级的信息。

信息过多

别再注释中添加有趣的历史性话题或无关的细节描述。

不明显的联系

注释及其描述的代码之间的联系应该是显而易见的。如果你不嫌麻烦要写注释,至少要让读者能看着注释和代码,并理解注释所谈何物。

注释的作用是解释未能自行解释的代码。如果注释本身还需要解释,那就太遗憾了。

函数头

短函数不需要太多描述。为只做一件事的短函数选个好名字,通常要比写函数头注释要好。


Comments: